// import { sumPointLength, getType, diffObject } from "./util";
// import { stateEnum, flexDirectionEnum, pointDomParam } from "./types/index";
export function getVersion(): string {
  let version = "0.1.4";
  console.log(`%c version %c ${version} `,
    'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px;  color: #fff',
    'background:#41b883 ; padding: 1px; border-radius: 0 3px 3px 0;  color: #fff');
  return version;
}

//init
function init(parentDomOrId: string | HTMLElement): GroupPathAnimation {
  if (parentDomOrId == null || parentDomOrId == undefined || parentDomOrId == "") {
    throw new Error("parentDomOrId is null or undefined");
  }
  let parent: HTMLElement;
  if (getType(parentDomOrId) == "String") {
    parent = document.getElementById(parentDomOrId as string) as HTMLElement;
  } else {
    parent = parentDomOrId as HTMLElement;
  }
  return new GroupPathAnimation(parent);
}

//group path animation
class GroupPathAnimation {
  isDisposed: boolean = false;
  options: pointDomParam[] = [];
  oldOptions: pointDomParam[] = [];
  parentDom: HTMLElement;
  groupPoints: PathAnimation[] = [];
  groupMap: groupMap | null = null;
  /**
   *
   */
  constructor(parentDom: HTMLElement) {
    this.parentDom = parentDom;

  }
  setOption(options: pointDomParam[], showIndexLabel: boolean = false) {
    if (options.length === 0) {
      throw new Error("options is empty");
    }

    if (this.options.length === 0) {
      this.options = options.map(p => ({ ...p })).slice(0);
    } else {
      this.oldOptions = this.options.map(p => ({ ...p })).slice(0);
      this.options = options.slice(0);
    }
    if (this.groupPoints.length === 0) {
      this.groupPoints = this.options.map((option) => {
        let pa = new PathAnimation(this.parentDom);
        pa.setOption(option, showIndexLabel);
        return pa;
      });
    } else {
      this.options.forEach((o, i) => {
        if (i >= this.groupPoints.length) {
          let pa = new PathAnimation(this.parentDom);
          pa.setOption(o, showIndexLabel);
          this.groupPoints.push(pa);
        } else {
          this.groupPoints[i].setOption(o, showIndexLabel)
        }
      });
      if (this.options.length < this.groupPoints.length) {
        for (let i = this.options.length; i < this.groupPoints.length; i++) {
          this.groupPoints[i].dispose();
        }
        this.groupPoints.splice(this.options.length, this.groupPoints.length - this.options.length);
      }
    }
    this.groupMap = {} as groupMap;
    let hasGroup = false;
    this.groupPoints.forEach(pa => {
      if (this.groupMap != null) {
        if (pa.groupName) {
          let map = this.groupMap[pa.groupName];
          if (map) {
            if (map.allRunning) {
              map.allRunning = pa.state == stateEnum.running;
            }
            if (pa.isGroupMain) {
              map.main = pa;
              map.direction = pa.options?.groupMainFlexDirection ?? flexDirectionEnum.end;
            } else {
              map.values.push(pa);
            }
          } else {
            let groupMap = {
              allRunning: pa.state == stateEnum.running,
              groupName: pa.groupName,
              values: [pa],
            } as groupValueMap;
            if (pa.isGroupMain) {
              groupMap.direction = pa.options?.groupMainFlexDirection ?? flexDirectionEnum.end;
              groupMap.main = pa;
            }
            this.groupMap[pa.groupName] = groupMap;
            hasGroup = true;
          }
        }
      }
    })
    if (hasGroup) {
      updateGroupMainStartOrEnd(this.groupMap);
    }
  }

  /**
   * dispose AnimatedPoint instance
   */
  dispose() {
    this.groupPoints.forEach(p => p.dispose());
    this.groupPoints = [];
    this.options = [];
    this.oldOptions = [];
    this.isDisposed = true;
    this.groupMap = null;
  }

  /**
 * stopAnimatedPoint
 */
  public pause(names?: string[]) {
    if (names && names.length > 0) {
      this.groupPoints.forEach(p => {
        let has = names.find(s => s === p.name);
        if (has) {
          p.pause();
        }
      });
    } else {
      this.groupPoints.forEach(p => p.pause());
    }
    return this;
  }

  /**
   * resume animation
   * @returns 
   */
  public resume(names?: string[]) {
    if (names && names.length > 0) {
      this.groupPoints.forEach(p => {
        let has = names.find(s => s === p.name);
        if (has) {
          p.resume();
        }
      });
    } else {
      this.groupPoints.forEach(p => p.resume());
    }
    return this;
  }


  /**
   * hide
   */
  public hide(names?: string[]) {
    if (names && names.length > 0) {
      this.groupPoints.forEach(p => {
        let has = names.find(s => s === p.name);
        if (has) {
          p.hide();
        }
      });
    } else {
      this.groupPoints.forEach(p => p.hide());
    }
    return this;
  }

  /**
   * show
   */
  public show(names?: string[]) {
    if (names && names.length > 0) {
      this.groupPoints.forEach(p => {
        let has = names.find(s => s === p.name);
        if (has) {
          p.show();
        }
      });
    } else {
      this.groupPoints.forEach(p => p.show());
    }
    return this;
  }

  /**
   * add animationend event listener
   * @param callback 
   * @returns 
   */
  addAnimationEndCallback(name: string = "", callback: () => any) {
    if (name) {
      this.groupPoints.forEach(p => {
        if (p.name === name) {
          p.addAnimationEndCallback(callback);
        }
      });
    } else {
      this.groupPoints.forEach(gs => {
        gs.addAnimationEndCallback(callback);
      })
    }
    return this;
  }

  /**
   * add animationstart event listener
   * @param callback 
   * @returns 
   */
  addAnimationStartCallback(name: string = "", callback: () => any) {
    if (name) {
      this.groupPoints.forEach(p => {
        if (p.name === name) {
          p.addAnimationStartCallback(callback);
        }
      })
    } else {
      this.groupPoints.forEach(gs => {
        gs.addAnimationStartCallback(callback);
      })
    }
    return this;
  }

  /**
   * add animationiteration event listener
   * @param callback 
   * @returns 
   */
  addAnimationIterationCallback(name: string = "", callback: () => any) {
    if (name) {
      this.groupPoints.forEach(p => {
        if (p.name === name) {
          p.addAnimationIterationCallback(callback);
        }
      })
    } else {
      this.groupPoints.forEach(gs => {
        gs.addAnimationIterationCallback(callback);
      })
    }
    return this;
  }

  /**
   * bind event
   * @param name 
   * @param eventName 
   * @param listener 
   * @param options 
   * @returns 
   */
  on<K extends keyof HTMLElementEventMap>(name: string = "", eventName: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions) {
    if (name) {
      this.groupPoints.forEach(p => {
        if (p.name == name) {
          p.on(eventName, listener, options);
        }
      })
    } else {
      this.groupPoints.forEach(p => {
        p.on(eventName, listener, options);
      })
    }
    return this;
  }

  /**
   * unbind event
   * @param name 
   * @param eventName 
   * @param listener 
   */
  off<K extends keyof HTMLElementEventMap>(name: string = "", eventName: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any) {
    if (name) {
      this.groupPoints.forEach(p => {
        if (p.name == name) {
          p.off(eventName, listener);
        }
      })
    } else {
      this.groupPoints.forEach(p => {
        p.off(eventName, listener);
      })
    }
    return this;
  }
}

//update group main start or end
function updateGroupMainStartOrEnd(option: groupMap) {
  if (option == null) return;
  let groupKeys = Object.keys(option);
  if (groupKeys.length == 0) return;
  for (let i = 0; i < groupKeys.length; i++) {
    const gk = groupKeys[i];
    let group = option[gk];
    if (!group.allRunning) {
      if (group.values.length > 0) {
        if (group.direction === flexDirectionEnum.end) {
          let endPoint = group.values[group.values.length - 1];
          if (endPoint.state === stateEnum.running) {
            continue;
          }
          let lastPoint = group.values.reverse().find(p => p.state === stateEnum.running);
          if (lastPoint && lastPoint.options && group.main.options) {
            //update main end point
            let pathArray = clone(group.main.options.pathPointArray);
            if (pathArray) {
              //TODO: if lastPoint is not the pathArray last point, then should reduce main end point
              pathArray[pathArray.length - 1] = lastPoint.options.pathPointArray[0];
              updateMainPointStartEnd(group.main, pathArray);
            }
          }
        }
      }
    }
  }
}

//update main point start end
function updateMainPointStartEnd(point: PathAnimation, pathPointArray: number[][]) {
  if (!point || !point.options || !point.options.pathPointArray) return;
  // point.options.pathPointArray = points;
  let path = generateLinePathFromPointArray(...pathPointArray);
  let option = { ...point.options };
  option.path = path;
  option.pathPointArray = pathPointArray;
  let oldLength = sumPointLength(point.options.pathPointArray);
  let newLength = sumPointLength(pathPointArray);
  option.animationDuration = +(newLength / oldLength * point.options.animationDuration).toFixed(2);
  option.num = Math.round(newLength / oldLength * point.options.num);
  option.num = option.num > 0 ? option.num : 1;
  if (option.num != point.options.num) {
    point.dispose();
    option.state = stateEnum.running;
    point.options = null;
    point.setOption(option);
  }
}

class PathAnimation {
  // options: pointDomParam;
  options: pointDomParam | null = null;
  oldOptions: pointDomParam | null = null;
  pointDoms: HTMLElement[] = [];
  name: string | undefined;
  groupName: string | undefined;
  isGroupMain: boolean | undefined;
  state: stateEnum = stateEnum.ready;
  parentDom: HTMLElement;
  /**
   *
   */
  constructor(parentDom: HTMLElement) {
    this.parentDom = parentDom;
  }

  /**
   * setOption
   */
  public setOption(option: pointDomParam, showIndexLabel: boolean = false) {
    if (!option.state) {
      option.state = stateEnum.running;
    }
    this.state = option.state;
    if (this.options == null) {
      this.options = { ...option };
      this.oldOptions = null;
    } else {
      this.oldOptions = { ...this.options };
      this.options = { ...option };
      update(this.oldOptions, this.options, this.pointDoms);
      return;
    }
    this.name = option.name;
    this.groupName = option.groupName;
    this.isGroupMain = option.isGroupMain;

    if (!option.path || option.path.length === 0) {
      if (option.pathPointArray) {
        option.path = generateLinePathFromPointArray(...option.pathPointArray);
      }
    }
    if (!option.path || !option.pathPointArray) {
      throw new Error("path or pathPointArray is required");
    }
    insertAnimationClassIntoHead(option.keyframes, option.keyframeName);
    let interval = +(option.animationDuration / option.num).toFixed(2);
    for (let i = 0; i < option.num; i++) {
      let pointDom = create(option, this.parentDom);
      pointDom.style.animationDelay = (option.animationDelay + (-i * interval)) + "s";
      if (showIndexLabel) {
        let indexLabel = (this.name ? this.name + ":" : "") + i;
        let indexDiv = document.createElement("div") as HTMLDivElement;
        indexDiv.innerHTML = indexLabel;
        pointDom.appendChild(indexDiv);
      }
      pointDom.dataset.index = i.toString();
      this.pointDoms.push(pointDom);
    }
    if (this.state === stateEnum.running) {
      runAnimation(this.pointDoms);
    }
    return this;
  }

  /**
   * destory AnimatedPoint
   */
  public dispose() {
    disposeAnimatedPoint(...this.pointDoms);
    this.state = stateEnum.destroyed;
    return this;
  };

  /**
   * stopAnimatedPoint
   */
  public pause() {
    pauseAnimatedPoint(...this.pointDoms);
    this.state = stateEnum.pause;
    return this;
  }

  /**
   * resume animation
   * @returns 
   */
  public resume() {
    resumeAnimatedPoint(...this.pointDoms);
    this.state = stateEnum.running;
    return this;
  }


  /**
   * hide
   */
  public hide() {
    this.pointDoms.forEach(p => {
      p.style.display = "none";
    });
    this.state = stateEnum.hidden;
    return this;
  }

  /**
   * show
   */
  public show() {
    this.pointDoms.forEach(p => {
      p.style.display = "block";
    });
    this.state = stateEnum.running;
    return this;
  }

  /**
   * on event
   */
  public on<K extends keyof HTMLElementEventMap>(eventName: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, options?: boolean | AddEventListenerOptions) {
    this.pointDoms.forEach(p => {
      p.addEventListener(eventName, listener, options)
    })
  }

  /**
   * off
   */
  public off<K extends keyof HTMLElementEventMap>(eventName: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any) {
    this.pointDoms.forEach(p => {
      p.removeEventListener(eventName, listener);
    })
  }

  /**
   * add animationend event listener
   * @param callback 
   * @returns 
   */
  addAnimationEndCallback(callback: () => void) {
    this.pointDoms.forEach(p => {
      p.addEventListener("animationend", callback);
    })
    return this;
  }

  /**
   * add animationstart event listener
   * @param callback 
   * @returns 
   */
  addAnimationStartCallback(callback: () => void) {
    this.pointDoms.forEach(p => {
      p.addEventListener("animationstart", callback);
    })
    return this;
  }

  /**
   * add animationiteration event listener
   * @param callback 
   * @returns 
   */
  addAnimationIterationCallback(callback: () => void) {
    this.pointDoms.forEach(p => {
      p.addEventListener("animationiteration", callback);
    })
    return this;
  }
}
/**
 * destroy animate
 * @param  {...HTMLElement} pointDoms
 */
function disposeAnimatedPoint(...pointDoms: HTMLElement[]): void {
  for (let index = 0; index < pointDoms.length; index++) {
    const point = pointDoms[index];
    point.style.animationPlayState = "paused";
    point.parentElement?.removeChild(point);
  }
  //remove referenced
  pointDoms = [];
}

/**
 * stop animate
 * @param  {...HTMLElement} pointDoms 
 */
function pauseAnimatedPoint(...pointDoms: HTMLElement[]): void {
  pointDoms.forEach(p => {
    p.style.animationPlayState = "paused";
  })
}

/**
 * resume animate
 * @param pointDoms 
 */
function resumeAnimatedPoint(...pointDoms: HTMLElement[]): void {
  pointDoms.forEach(p => {
    p.style.animationPlayState = "running";
  })
}

/**
 * Gets the width and height of the dom element
 * @param {id} id 
 * @returns [width, height]
 */
function getImgWithHeight(id: string = ""): number[] {
  let img = document.getElementById(id) as HTMLImageElement;
  return [img.width, img.height];
}

/**
 * innert animation keyframes into body
 * @param keyframes animation keyframes
 */
function insertAnimationClassIntoHead(keyframes?: string, styleId: string = "path_default_keyframes") {
  let style = document.getElementById(styleId) as HTMLStyleElement;
  if (style) {
    return;
  }
  style = document.createElement("style");
  style.id = styleId;
  if (keyframes) {
    style.innerHTML = keyframes;
  } else {
    style.type = "text/css";
    style.innerHTML = `
    @keyframes flow {
      0% {
        offset-distance: 0%;
      }
      100% {
        offset-distance: 100%;
      }
    }
  `;
  }
  document.head.appendChild(style);
}

//get keyframes from name
function getKeyFrames(name: string): CSSStyleSheet | undefined {
  //get all style
  var ss = document.styleSheets;
  for (var i = 0; i < ss.length; i++) {
    const item = ss[i];
    let rules = Array.from(item.cssRules);
    for (let index = 0; index < rules.length; index++) {
      let rule = rules[index] as CSSKeyframesRule;
      if (rule && rule.name === name) {
        return item;
      }
    }
  }
}

/**
 * Generate routes based on point location locations
 * @returns path string
 */
function generateLinePathFromPointArray(...points: number[][]): string {
  if (points.length < 2) {
    console.warn("start or end point is not 2d array");
    return "";
  }

  let path = "";
  for (let index = 0; index < points.length; index++) {
    const point = points[index];
    if (index == 0) {
      path = `M ${point[0]} ${point[1]}`;
    } else {
      path += ` L ${point[0]} ${point[1]}`;
    }
  }
  return path;
}

/**
 * create point dom
 * @returns
 */
function create(options: pointDomParam, parentDom: HTMLElement): HTMLElement {
  let { offsetPoint, width, height, imageSrc, zIndex } = options;
  // let parentDom: HTMLElement;
  // if (getType(parentDomOrId) == "String") {
  //   parent = document.getElementById(parentDomOrId as string) as HTMLElement;
  // } else {
  //   parent = parentDomOrId as HTMLElement;
  // }

  if (parentDom == null) {
    throw new Error("parent dom is not exist");
  }
  let pointDom = document.createElement("div");
  // let pointId = "__path_animate_id_" + Math.random().toString(36).substr(2);
  // pointDom.id = pointId;
  pointDom.dataset.name = options.name;
  pointDom.style.position = "absolute";
  if (offsetPoint) {
    pointDom.style.left = `${offsetPoint[0] ?? 0}px`;
    pointDom.style.top = `${offsetPoint[1] ?? 0}px`;
  }
  pointDom.style.width = width + "px";
  pointDom.style.height = height + "px";
  if (imageSrc) {
    var img = document.createElement("img");
    img.src = imageSrc;
    img.style.width = "100%";
    img.style.height = "100%";
    pointDom.appendChild(img);
  } else {
    pointDom.style.background = (options.background !== "" && options.background) ? options.background : "linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #33ff00)";
  }
  pointDom.style.zIndex = zIndex.toString();
  // pointDom.style.animation = "flow 4s linear infinite";
  pointDom.style.animationName = options.animationName ?? "flow";
  pointDom.style.animationTimingFunction = options.animationTimingFunction ?? "linear";
  pointDom.style.animationDirection = options.animationDirection ?? "normal";
  pointDom.style.animationFillMode = options.animationFillMode ?? "none";
  pointDom.style.animationIterationCount = options.animationIterationCount ?? "infinite";
  pointDom.style.animationDuration = (options.animationDuration ?? 1) + "s";
  pointDom.style.animationDelay = (options.animationDelay ?? 0) + "s";
  pointDom.style.animationPlayState = "paused";
  // pointDom.style.animationPlayState = animationStyle.animationPlayState ?? "running";
  pointDom.style.willChange = "offset-path";
  pointDom.style.offsetPath = `path('${options.path ?? ""}')`;
  parentDom.appendChild(pointDom);
  return pointDom;
}

/**
 * update point dom
 * @param oldOptions 
 * @param newOptions 
 * @param points 
 * @returns 
 */
function update(oldOptions: pointDomParam | null, newOptions: pointDomParam | null, points: HTMLElement[]) {
  if (newOptions == null) return;
  if (oldOptions == null) {
    console.warn("no data changed");
    return;
  }
  switch (newOptions.state) {
    case stateEnum.ready:
    case stateEnum.pause:
    case stateEnum.running:
      points.forEach(p => {
        p.style.display = "block";
      })
      break;
    case stateEnum.destroyed:
    case stateEnum.hidden:
      points.forEach(p => {
        p.style.display = "none";
      })
      break;
    default:
      break;
  }

  let diff = diffObject<pointDomParam>(oldOptions, newOptions);
  if (diff == null) {
    return;
  }
  let inteval = 0;
  if (diff["num"]) {
    inteval = +(newOptions.animationDuration / newOptions.num).toFixed(2);
    //reload point

  }
  for (let i = 0; i < points.length; i++) {
    const point = points[i];
    for (let key in diff) {
      switch (key) {
        case "name":
          point.dataset.name = newOptions.name;
          break;
        case "offsetPoint":
          if (newOptions.offsetPoint) {
            point.style.left = `${newOptions.offsetPoint[0] ?? 0}px`;
            point.style.top = `${newOptions.offsetPoint[1] ?? 0}px`;
          }
          point
          break;
        case "width":
          point.style.width = newOptions.width + "px";
          break;
        case "height":
          point.style.height = newOptions.height + "px";
          break;
        case "imageSrc":
          if (newOptions.imageSrc) {
            if (point.firstChild && getType(point.firstChild) === "HTMLImageElement") {
              (point.firstChild as HTMLImageElement).src = newOptions.imageSrc;
            }
          } else {
            if (point.firstChild && getType(point.firstChild) === "HTMLImageElement") {
              point.removeChild(point.firstChild);
            }
          }
          break;
        case "zIndex":
          point.style.zIndex = newOptions.zIndex.toString();
          break;
        case "background":
          if (newOptions.imageSrc) {
            break;
          } else {
            if (point.firstChild && getType(point.firstChild) === "HTMLImageElement") {
              point.removeChild(point.firstChild);
            }
          }
          point.style.background = (newOptions.background !== "" && newOptions.background) ? newOptions.background : "linear-gradient(to right, rgba(255, 255, 255, 0) 0%, #33ff00)";
          break;
        case "animationName":
          point.style.animationName = newOptions.animationName ?? "flow";
          break;
        case "animationTimingFunction":
          point.style.animationTimingFunction = newOptions.animationTimingFunction ?? "linear";
          break;
        case "animationDirection":
          point.style.animationDirection = newOptions.animationDirection ?? "normal";
          break;
        case "animationFillMode":
          point.style.animationFillMode = newOptions.animationFillMode ?? "none";
          break;
        case "animationIterationCount":
          point.style.animationIterationCount = newOptions.animationIterationCount ?? "infinite";
          break;
        case "animationDuration":
          point.style.animationDuration = (newOptions.animationDuration ?? 1) + "s";
          break;
        case "animationDelay":
          point.style.animationDelay = (newOptions.animationDelay + (-i * inteval)) + "s";
          break;
        case "path":
          point.style.offsetPath = `path('${newOptions.path ?? ""}')`;
          break;
        default:
          break;
      }
    }
  }
}


//#region interface and types
/**
 * animate state
 */
export enum stateEnum {
  ready = "ready",
  running = "running",
  pause = "pause",
  hidden = "hidden",
  destroyed = "destroyed"
}

/**
 * fluid flex start or end
 */
export enum flexDirectionEnum {
  start = "start",
  end = "end"
}


/**
 * create animate option interface
 */
export interface pointDomParam {
  //animation dom name
  name: string,
  //group name
  groupName: string,
  //group is main
  isGroupMain?: boolean;
  groupMainFlexDirection?: flexDirectionEnum;
  //path animation state
  state: stateEnum,
  //animation dom offset
  offsetPoint?: number[];
  //animation path
  path: string;
  //animation path point array, it will be generate path
  pathPointArray: Array<number[]>;
  //this path point number
  num: number;
  //animation dom width
  width: number;
  //animation dom height
  height: number;
  //animation dom zIndex
  zIndex: number;
  //animate image path  or base64, If there is a picture, the picture is preferred
  imageSrc?: string;
  //animate style - background color ,If there is a picture, it will not take effect
  background?: string;
  //animation keyframes
  keyframes?: string;
  //animation style keyframe name
  keyframeName?: string;
  //animation duration seconds
  animationDuration: number;
  //animation timing function
  animationTimingFunction: string;
  //animation delay seconds
  animationDelay: number;
  //animation iteration count
  animationIterationCount: string;
  //animation direction
  animationDirection: string;
  //animation fill mode
  animationFillMode: string;
  //animation name
  animationName: string;
}



/**
 * start animate
 * @param {*} pointDoms 
 */
function runAnimation(pointDoms: HTMLElement[]): void {
  pointDoms.forEach(p => {
    p.style.display = "block";
    p.style.animationPlayState = "running";
  })
}

export type groupValueMap = {
  allRunning: boolean;
  groupName: string;
  direction: flexDirectionEnum;
  values: PathAnimation[],
  main: PathAnimation
}

export type groupMap = {
  [key: string]: groupValueMap
}
//#endregion

//#region utils
//calc all point length
export function sumPointLength(pathPointArray: number[][]): number {
  let sum = 0;
  if (pathPointArray.length <= 1) return 0;
  for (let i = 0; i < pathPointArray.length - 1; i++) {
    const point = pathPointArray[i];
    const nextPoint = pathPointArray[i + 1];
    const x = nextPoint[0] - point[0];
    const y = nextPoint[1] - point[1];
    sum += Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
  }
  return +sum.toFixed(2);
}

// diff two object key-value
export function diffObject<T>(oldObj: T, newObj: T): T | null {
  let result: T | null = null;
  for (let key in oldObj) {
    if (oldObj[key] !== newObj[key]) {
      if (result == null) result = {} as T;
      result[key] = newObj[key];
    }
  }
  return result;
}

//get object type
export function getType(obj: any) {
  return Object.prototype.toString.call(obj).slice(8, -1);
}

export function clone(obj: any) {
  return JSON.parse(JSON.stringify(obj));
}
//#endregion

export default {
  init: init
};